<!DOCTYPE html>
<html class="has-navbar-fixed-top">
<head>
    <meta charset="utf-8">
<title>集成第三方组件 - wanzixin</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">

<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/outdated-browser/1.1.5/outdatedbrowser.min.css">


<link href="/Study/Java/Spring/zh-cn/%E9%9B%86%E6%88%90%E7%AC%AC%E4%B8%89%E6%96%B9%E7%BB%84%E4%BB%B6/" rel="alternate" hreflang="en" />
    


<meta name="description" content="">





    <meta name="description" content="Spring框架不仅提供了标准的IoC容器、AOP支持、数据库支持以及WebMVC等标准功能，还可以非常方便地集成许多常用的">
<meta property="og:type" content="article">
<meta property="og:title" content="集成第三方组件">
<meta property="og:url" content="https://wanzixin.github.io/Study/Java/Spring/zh-cn/%E9%9B%86%E6%88%90%E7%AC%AC%E4%B8%89%E6%96%B9%E7%BB%84%E4%BB%B6/index.html">
<meta property="og:site_name" content="wanzixin">
<meta property="og:description" content="Spring框架不仅提供了标准的IoC容器、AOP支持、数据库支持以及WebMVC等标准功能，还可以非常方便地集成许多常用的">
<meta property="og:locale" content="zh_CN">
<meta property="article:published_time" content="2021-07-11T02:18:09.000Z">
<meta property="article:modified_time" content="2021-07-13T11:35:36.063Z">
<meta property="article:author" content="wanzixin">
<meta name="twitter:card" content="summary">





<link rel="icon" href="/favicon.png">


<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Ovo|Source+Code+Pro">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/bulma/0.6.2/css/bulma.min.css">


<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/lightgallery/1.6.8/css/lightgallery.min.css">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/justifiedGallery/3.6.5/css/justifiedGallery.min.css">


<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/atom-one-light.min.css">


<link rel="stylesheet" href="/css/style.css">


<script defer src="//use.fontawesome.com/releases/v5.0.8/js/all.js"></script>


    
    
    
    
    
    
    
    
    
    

    


<meta name="generator" content="Hexo 5.4.0"></head>
<body>
    
<nav class="navbar is-transparent is-fixed-top navbar-main" role="navigation" aria-label="main navigation">
    <div class="container">
        <div class="navbar-brand">
            <a class="navbar-item navbar-logo" href="/zh-cn">
                
                    
                    wanzixin
                    
                
            </a>
            <div class="navbar-burger">
                <span></span>
                <span></span>
                <span></span>
            </div>
        </div>
        
        <div class="navbar-menu navbar-start">
            
            <a class="navbar-item "
               href="/archives">Archives</a>
            
            <a class="navbar-item "
               href="/categories">Categories</a>
            
            <a class="navbar-item "
               href="/categories/Diary">Diary</a>
            
            <a class="navbar-item "
               href="/categories/Gallery">Gallery</a>
            
            <a class="navbar-item "
               href="/categories/Study">Study</a>
            
            <a class="navbar-item "
               href="/categories/Item">Item</a>
            
            <a class="navbar-item "
               href="/about">About</a>
            
        </div>
        
        <div class="navbar-menu navbar-end">
            
            <a class="navbar-item search" title="搜索" href="javascript:;">
                <i class="fas fa-search"></i>
            </a>
            
            
            <div class="navbar-item is-hoverable has-dropdown is-hidden-mobile is-hidden-tablet-only toc">
                <a class="navbar-item" title="目录">
                    <i class="fa fa-list"></i>
                </a>
                <div class="navbar-dropdown is-right">
                    
                    
                    
                    
                    <a class="navbar-item" href="#集成JavaMail">1&nbsp;&nbsp;<b>集成JavaMail</b></a>
                    
                    
                    <hr class="navbar-divider">
                    
                    
                    <a class="navbar-item" href="#集成JMS">2&nbsp;&nbsp;<b>集成JMS</b></a>
                    
                    
                    
                    <a class="navbar-item" href="#小结">2.1&nbsp;&nbsp;小结</a>
                    
                    
                    <hr class="navbar-divider">
                    
                    
                    <a class="navbar-item" href="#使用Scheduler">3&nbsp;&nbsp;<b>使用Scheduler</b></a>
                    
                    
                    
                    <a class="navbar-item" href="#使用Cron任务">3.1&nbsp;&nbsp;使用Cron任务</a>
                    
                    
                    
                    <a class="navbar-item" href="#集成Quartz">3.2&nbsp;&nbsp;集成Quartz</a>
                    
                    
                    <hr class="navbar-divider">
                    
                    
                    <a class="navbar-item" href="#集成JMX">4&nbsp;&nbsp;<b>集成JMX</b></a>
                    
                </div>
            </div>
            
            
            <a class="navbar-item" title="GitHub" target="_blank" rel="noopener" href="https://github.com/wanzixin">
                
                <i class="fab fa-github"></i>
                
            </a>
               
            
        </div>
    </div>
</nav>

    <section class="section">
    <div class="container">
    <article class="article content gallery" itemscope itemprop="blogPost">
    <h1 class="article-title is-size-3 is-size-4-mobile" itemprop="name">
        
            集成第三方组件
        
    </h1>
    <div class="article-meta columns is-variable is-1 is-multiline is-mobile is-size-7-mobile">
        <span class="column is-narrow">
            
                <span>7月 11 2021</span>
            
        </span>
        
        <span class="column is-narrow article-category">
            <i class="far fa-folder"></i>
            <a class="article-category-link" href="/zh-cn/categories/Study/">Study</a><span>></span><a class="article-category-link" href="/zh-cn/categories/Study/Java/">Java</a><span>></span><a class="article-category-link" href="/zh-cn/categories/Study/Java/Spring/">Spring</a>
        </span>
        
        
        <span class="column is-narrow">
            
            
            1 小时 读完 (约 7771 字)
        </span>
        
    </div>
    <div class="article-entry is-size-6-mobile" itemprop="articleBody">
    
        <html><head></head><body><p>Spring框架不仅提供了标准的IoC容器、AOP支持、数据库支持以及WebMVC等标准功能，还可以非常方便地集成许多常用的<span id="more"></span>第三方组件：</p>
<ul>
<li>可以集成JavaMail发送邮件</li>
<li>可以集成JMS消息服务</li>
<li>可以集成Quartz实现定时任务</li>
<li>可以集成Redis等服务</li>
</ul>
<p>本章我们介绍如何在Spring中简单快捷地集成这些第三方组件。</p>
<h2 id="集成JavaMail"><a href="#集成JavaMail" class="headerlink" title="集成JavaMail"></a>集成JavaMail</h2><p>在Spring中可以集成JavaMail。在服务器端，我们主要以发送邮件为主，例如在注册成功、登录时、购物付款后通知用户，基本上不会遇到接收用户邮件的情况，所以本节我们只讨论如何在Spring发送邮件。</p>
<p>在Spring中，发送邮件最终也是需要JavaMail，Spring只对JavaMail做了一点简单封装。为了在Spring中集成JavaMail，我们在<code>pom.xml</code>中添加Web相关依赖以及如下依赖：</p>
<ul>
<li>org.springframework:spring-context-support:5.2.0.RELEASE</li>
<li>javax.mail:javax.mail-api:1.6.2</li>
<li>com.sun.mail:javax.mail:1.6.2</li>
</ul>
<p>我们希望用户在注册成功后能收到注册邮件，为此，我们先定义一个<code>JavaMailSender</code>的Bean：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Bean</span><br><span class="hljs-function">JavaMailSender <span class="hljs-title">createJavaMailSender</span><span class="hljs-params">(</span></span><br><span class="hljs-params"><span class="hljs-function">        // smtp.properties:</span></span><br><span class="hljs-params"><span class="hljs-function">        <span class="hljs-meta">@Value("${smtp.host}")</span> String host,</span></span><br><span class="hljs-params"><span class="hljs-function">        <span class="hljs-meta">@Value("${smtp.port}")</span> <span class="hljs-keyword">int</span> port,</span></span><br><span class="hljs-params"><span class="hljs-function">        <span class="hljs-meta">@Value("${smtp.auth}")</span> String auth,</span></span><br><span class="hljs-params"><span class="hljs-function">        <span class="hljs-meta">@Value("${smtp.username}")</span> String username,</span></span><br><span class="hljs-params"><span class="hljs-function">        <span class="hljs-meta">@Value("${smtp.password}")</span> String password,</span></span><br><span class="hljs-params"><span class="hljs-function">        <span class="hljs-meta">@Value("${smtp.debug:true}")</span> String debug)</span></span><br><span class="hljs-function"></span>{<br>    <span class="hljs-keyword">var</span> mailSender = <span class="hljs-keyword">new</span> JavaMailSenderImpl();<br>    mailSender.setHost(host);<br>    mailSender.setPort(port);<br>    mailSender.setUsername(username);<br>    mailSender.setPassword(password);<br>    Properties props = mailSender.getJavaMailProperties();<br>    props.put(<span class="hljs-string">"mail.transport.protocol"</span>, <span class="hljs-string">"smtp"</span>);<br>    props.put(<span class="hljs-string">"mail.smtp.auth"</span>, auth);<br>    <span class="hljs-keyword">if</span> (port == <span class="hljs-number">587</span>) {<br>        props.put(<span class="hljs-string">"mail.smtp.starttls.enable"</span>, <span class="hljs-string">"true"</span>);<br>    }<br>    <span class="hljs-keyword">if</span> (port == <span class="hljs-number">465</span>) {<br>        props.put(<span class="hljs-string">"mail.smtp.socketFactory.port"</span>, <span class="hljs-string">"465"</span>);<br>        props.put(<span class="hljs-string">"mail.smtp.socketFactory.class"</span>, <span class="hljs-string">"javax.net.ssl.SSLSocketFactory"</span>);<br>    }<br>    props.put(<span class="hljs-string">"mail.debug"</span>, debug);<br>    <span class="hljs-keyword">return</span> mailSender;<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>这个JavaMailSender接口的实现类是JavaMainlSenderImpl，初始化时，传入的参数与JavaMail完全一致。另外注意到需要注入的属性是从<code>smtp.properties</code>中读取的，因此，AppConfig导入的就不止一个<code>.properties</code>文件，可以导入多个：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Configuration</span><br><span class="hljs-meta">@ComponentScan</span><br><span class="hljs-meta">@EnableWebMvc</span><br><span class="hljs-meta">@EnableTransactionManagement</span><br><span class="hljs-meta">@PropertySource({ "classpath:/jdbc.properties", "classpath:/smtp.properties" })</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppConfig</span> </span>{<br>    ...<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>下一步是封装一个<code>MailService</code>，并定义<code>sendRegistrationMail()</code>方法：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Component</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MailService</span> </span>{<br>    <span class="hljs-meta">@Value("${smtp.from}")</span><br>    String from;<br><br>    <span class="hljs-meta">@Autowired</span><br>    JavaMailSender mailSender;<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">sendRegistrationMail</span><span class="hljs-params">(User user)</span> </span>{<br>        <span class="hljs-keyword">try</span> {<br>            MimeMessage mimeMessage = mailSender.createMimeMessage();<br>            MimeMessageHelper helper = <span class="hljs-keyword">new</span> MimeMessageHelper(mimeMessage, <span class="hljs-string">"utf-8"</span>);<br>            helper.setFrom(from);<br>            helper.setTo(user.getEmail());<br>            helper.setSubject(<span class="hljs-string">"Welcome to Java course!"</span>);<br>            String html = String.format(<span class="hljs-string">"&lt;p&gt;Hi, %s,&lt;/p&gt;&lt;p&gt;Welcome to Java course!&lt;/p&gt;&lt;p&gt;Sent at %s&lt;/p&gt;"</span>, user.getName(), LocalDateTime.now());<br>            helper.setText(html, <span class="hljs-keyword">true</span>);<br>            mailSender.send(mimeMessage);<br>        } <span class="hljs-keyword">catch</span> (MessagingException e) {<br>            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> RuntimeException(e);<br>        }<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>观察上述代码，<code>MimeMessage</code>是JavaMail的邮件对象，而<code>MimeMessageHelper</code>是Spring提供的用于简化设置<code>MimeMessage</code>的类，比如我们设置HTML邮件就可以直接调用<code>setText(String text, boolean html)</code>，而不必再调用比较繁琐的JavaMail接口方法。最后一步，调用<code>JavaMailSender.send()</code>方法把邮件发送出去。</p>
<p>在MVC的某个Controller某个方法中，当用户注册成功后，我们就启动一个新线程来异步来发送邮件：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs java">User user = userService.register(email, password, name);<br>logger.info(<span class="hljs-string">"user registered: {}"</span>, user.getEmail());<br><span class="hljs-comment">// send registration mail:</span><br><span class="hljs-keyword">new</span> Thread(() -&gt; {<br>    mailService.sendRegistrationMail(user);<br>}).start();<br></code></pre></td></tr></tbody></table></figure>

<p>因为发送邮件是一种耗时的任务，从几秒到几分钟不等，因此，异步发送是保证页面能快速显示的必要措施。这里我们新起了一个线程，但实际上还有更优化的方法，我们下一节讨论。</p>
<h2 id="集成JMS"><a href="#集成JMS" class="headerlink" title="集成JMS"></a>集成JMS</h2><p>JMS即Java Message Service，是JavaEE的消息服务接口，JMS主要有两个版本1.1和2.0，2.0相比主要是简化了收发消息的代码。</p>
<p>所谓消息服务，就是两个进程之间，通过消息服务器传递消息。使用消息服务，而不是直接调用对方的API的好处是：</p>
<ul>
<li>双方各自无需知晓对方的存在，消息可以异步处理，因为消息服务器会在Consumer离线的时候自动缓存消息</li>
<li>如果Producer发送的消息频率高于Consumer的处理能力，消息可以积压在消息服务器，不至于压垮Consumer</li>
<li>通过一个消息服务器，可以连接多个Producer和多个Consumer</li>
</ul>
<p>因为消息服务在各类应用程序中非常有用，所以JavaEE专门定义了JMS规范。注意到JMS是一组接口定义，如果我们要使用JMS，还需要选择一个具体的JMS产品。常用的JMS服务器有开源的ActiveMQ，商业服务器如WebLogic、WebSphere等也内置了JMS支持。这里我们选择开源的ActiveMQ作为JMS服务器，在开发JMS之前，我们先安装ActiveMQ。</p>
<blockquote>
<p>现在问题来了：从官网下载ActiveMQ时，蹦出一个页面，让我们选择ActiveMQ Classic或者ActiveMQ Artemis，这两个是什么关系，又有什么区别？</p>
<p>实际上ActiveMQ Classic原来就叫ActiveMQ，是Apache开发的基于JMS 1.1的消息服务器，目前稳定版本号是5.x，而ActiveMQ Artemis是由RedHat捐赠的<a target="_blank" rel="noopener" href="https://hornetq.jboss.org/">HornetQ</a>服务器代码的基础上开发的，目前稳定版本号是2.x。和ActiveMQ Classic相比，Artemis版的代码与Classic完全不同，并且，它支持JMS 2.0，使用基于Netty的异步IO，大大提升了性能。此外，Artemis不仅提供了JMS接口，它还提供了AMQP接口，STOMP接口和物联网使用的MQTT接口。选择Artemis，相当于一鱼四吃。</p>
<p>所以，我们这里直接选择ActiveMQ Artemis。从官网<a target="_blank" rel="noopener" href="https://activemq.apache.org/components/artemis/download/">下载</a>最新的2.x版本，解压后设置环境变量<code>ARTEMIS_HOME</code>，指向Artemis根目录，例如<code>C:\Apps\artemis</code>，然后，把<code>ARTEMIS_HOME/bin</code>加入PATH环境变量：</p>
<ul>
<li>Windows下添加<code>%ARTEMIS_HOME%\bin</code>到Path路径</li>
<li>Mac和Linux下添加<code>$ARTEMIS_HOME/bin</code>到PATH路径</li>
</ul>
</blockquote>
<p>Artemis有个很好的设计，就是它把程序和数据完全分离了。我们解压后的ARTEMIS_HOME目录是程序目录，要启动一个Artemis服务，还需要创建一个数据目录。我们把数据目录直接设定在项目<code>spring-integration-jms</code>的<code>jms-data</code>目录下。执行命令<code>artemis create jms-data</code>。</p>
<p>在创建过程中，会要求用户输入连接用户和口令，这里我们设定<code>admin</code>和<code>password</code>，以及是否允许匿名访问，选择<code>N</code>。此数据目录<code>jms-data</code>不仅包含消息数据、日志，还自动创建了两个启动服务器的命令<code>bin/artemis</code>和<code>bin/artemis-service</code>，前者在前台启动运行，按<code>Ctrl+C</code>结束，后者会一直在后台运行。</p>
<p>我们把目录切换到<code>jms-data/bin</code>，直接运行<code>artemis run</code>即可启动Artemis服务。启动成功后，Artemis提示可以通过URL<code>http://localhost:8161/console</code>访问管理后台，注意不要关闭命令行窗口。</p>
<p>在编写JMS代码前，首先得理解JMS的消息模型。JMS把生产消息的一方成为Producer，处理消息的一方称为Consumer。有两种类型的消息通道，一种是Queue：</p>
<figure class="highlight plaintext hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs ascii">┌────────┐    ┌────────┐    ┌────────┐<br>│Producer│───&gt;│ Queue  │───&gt;│Consumer│<br>└────────┘    └────────┘    └────────┘<br></code></pre></td></tr></tbody></table></figure>

<p>一种是Topic：</p>
<figure class="highlight plaintext hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs ascii">                            ┌────────┐<br>                         ┌─&gt;│Consumer│<br>                         │  └────────┘<br>┌────────┐    ┌────────┐ │  ┌────────┐<br>│Producer│───&gt;│ Topic  │─┼─&gt;│Consumer│<br>└────────┘    └────────┘ │  └────────┘<br>                         │  ┌────────┐<br>                         └─&gt;│Consumer│<br>                            └────────┘<br></code></pre></td></tr></tbody></table></figure>

<p>它们的区别在于，Queue是一对一的通道，如果Consumer离线无法处理消息时，Queue会把消息存起来，等Consumer连接的时候发给它。设定了持久化机制的Queue不会丢失消息。如果有多个Consumer接入同一个Queue，那么它们等效于以集群的方式处理消息。例如，发送方发送的消息是A，B，C，D，E，F，两个Consumer可能分别收到A，C，E和B，D，F，即每个消息只会交给其中一个Consumer处理。</p>
<p>Topic则是一种一对多的通道。一个Producer发送的消息会被多个Consumer同时接收到，即每个Consumer会收到一份完整的信息流。那么如果一个Consumer暂时离线，一段时间后又重新上线，那么在离线期间产生的消息还能不能收到？这取决于消息服务器对Topic类型消息的持久化机制，如果消息服务器不存储Topic消息，那么离线的Consumer会丢失部分离线时期的消息；如果消息服务器存储了Topic消息，那么离线的Consumer可以收到自上次离线后产生的所有消息。JMS规范通过Consumer指定一个持久化订阅可以在上线后收取所有离线期间的消息，如果指定的是非持久化订阅，那么离线期间的消息会全部丢失。</p>
<p>细心的朋友可能发现了，如果一个Topic的消息全部被持久化了，并且只有一个Consumer，那么它和Queue其实是一样的。实际上，很多消息服务器内部只有Topic类型的消息架构，Queue可以通过Topic“模拟”出来。</p>
<p>无论是Queue还是Topic，对Producer没什么要求。多个Producer也可以写入同一个Queue或Topic，此时消息服务器内部会自动排序确保消息总是有序的。</p>
<p>以上是消息服务的基本模型。具体到某个消息服务器时，Producer和Consumer通常是通过TCP协议连接消息服务器的。编写JMS程序时，又会遇到<code>ConnectionFactory</code>、<code>Connection</code>、<code>Session</code>等概念，其实这和JDBC连接是类似的：</p>
<ul>
<li>ConnectionFactory：代表一个到消息服务器的连接池，类似JDBC的DataSource</li>
<li>Connection：代表一个到消息服务器的连接，类似JDBC的Connection</li>
<li>Session：代表一个经过认证后的连接会话</li>
<li>Message：代表一个消息对象</li>
</ul>
<p>在JMS 1.1中，发送消息的典型代码如下：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">try</span> {<br>    Connection connection = <span class="hljs-keyword">null</span>;<br>    <span class="hljs-keyword">try</span> {<br>        <span class="hljs-comment">// 创建连接:</span><br>        connection = connectionFactory.createConnection();<br>        <span class="hljs-comment">// 创建会话:</span><br>        Session session = connection.createSession(<span class="hljs-keyword">false</span>,Session.AUTO_ACKNOWLEDGE);<br>        <span class="hljs-comment">// 创建一个Producer并关联到某个Queue:</span><br>        MessageProducer messageProducer = session.createProducer(queue);<br>        <span class="hljs-comment">// 创建一个文本消息:</span><br>        TextMessage textMessage = session.createTextMessage(text);<br>        <span class="hljs-comment">// 发送消息:</span><br>        messageProducer.send(textMessage);<br>    } <span class="hljs-keyword">finally</span> {<br>        <span class="hljs-comment">// 关闭连接:</span><br>        <span class="hljs-keyword">if</span> (connection != <span class="hljs-keyword">null</span>) {<br>            connection.close();<br>        }<br>    }<br>} <span class="hljs-keyword">catch</span> (JMSException ex) {<br>    <span class="hljs-comment">// 处理JMS异常</span><br>}<br></code></pre></td></tr></tbody></table></figure>

<p>JMS 2.0改进了一些API接口，其中，<code>JMSContext</code>实现了<code>AutoCloseable</code>接口，可以使用<code>try(resource)</code>语法，代码更简单。发送消息变得更简单：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">try</span> (JMSContext context = connectionFactory.createContext()) {<br>    context.createProducer().send(queue, text);<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>有了以上预备知识，我们就可以开发开发JMS应用了。</p>
<p>首先，在<code>pom.xml</code>中添加依赖：</p>
<ul>
<li>org.springframework:spring-jms:5.2.0.RELEASE</li>
<li>javax.jms:javax.jms-api:2.0.1</li>
<li>org.apache.activemq:artemis-jms-client:2.13.0</li>
<li>io.netty:netty-handler-proxy:4.1.45.Final</li>
</ul>
<p>在AppConfig上添加<code>@EnableJms</code>让Spring自动扫描JMS相关的Bean，并加载JMS配置文件<code>jms.properties</code>：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Configuration</span><br><span class="hljs-meta">@ComponentScan</span><br><span class="hljs-meta">@EnableWebMvc</span><br><span class="hljs-meta">@EnableJms</span> <span class="hljs-comment">// 启用JMS</span><br><span class="hljs-meta">@EnableTransactionManagement</span><br><span class="hljs-meta">@PropertySource({ "classpath:/jdbc.properties", "classpath:/jms.properties" })</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppConfig</span> </span>{<br>    ...<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>首先创建的Bean是ConnectionFactory，即连接消息服务器的连接池：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Bean</span><br><span class="hljs-function">ConnectionFactory <span class="hljs-title">createJMSConnectionFactory</span><span class="hljs-params">(</span></span><br><span class="hljs-params"><span class="hljs-function">    <span class="hljs-meta">@Value("${jms.uri:tcp://localhost:61616}")</span> String uri,</span></span><br><span class="hljs-params"><span class="hljs-function">    <span class="hljs-meta">@Value("${jms.username:admin}")</span> String username,</span></span><br><span class="hljs-params"><span class="hljs-function">    <span class="hljs-meta">@Value("${jms.password:password}")</span> String password)</span></span><br><span class="hljs-function"></span>{<br>    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ActiveMQJMSConnectionFactory(uri, username, password);<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>因为我们使用的消息服务器是ActiveMQ Artemis，所以<code>ConnectionFactory</code>的实现类就是消息服务器提供的<code>ActiveMQJMSConnectionFactory</code>，它需要的参数均由配置文件读取后传入，并设置默认值。</p>
<p>我们再创建一个<code>JmsTemplate</code>，它是Spring提供的一个工具类，和<code>JdbcTemplate</code>类似，可以简化发送消息的代码：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Bean</span><br><span class="hljs-function">JmsTemplate <span class="hljs-title">createJmsTemplate</span><span class="hljs-params">(<span class="hljs-meta">@Autowired</span> ConnectionFactory connectionFactory)</span> </span>{<br>    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> JmsTemplate(connectionFactory);<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>下一步要创建的是<code>JmsListenerContainerFactory</code>，</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Bean("jmsListenerContainerFactory")</span><br><span class="hljs-function">DefaultJmsListenerContainerFactory <span class="hljs-title">createJmsListenerContainerFactory</span><span class="hljs-params">(<span class="hljs-meta">@Autowired</span> ConnectionFactory connectionFactory)</span> </span>{<br>    <span class="hljs-keyword">var</span> factory = <span class="hljs-keyword">new</span> DefaultJmsListenerContainerFactory();<br>    factory.setConnectionFactory(connectionFactory);<br>    <span class="hljs-keyword">return</span> factory;<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>除了必须指定Bean的名称是<code>jmsListenerContainerFactory</code>外，这个Bean的作用是处理和Consumer相关的Bean。我们先跳过它的原理，继续编写<code>MessagingService</code>来发送消息：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Component</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MessagingService</span> </span>{<br>    <span class="hljs-meta">@Autowired</span> ObjectMapper objectMapper;<br>    <span class="hljs-meta">@Autowired</span> JmsTemplate jmsTemplate;<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">sendMailMessage</span><span class="hljs-params">(MailMessage msg)</span> <span class="hljs-keyword">throws</span> Exception </span>{<br>        String text = objectMapper.writeValueAsString(msg);<br>        jmsTemplate.send(<span class="hljs-string">"jms/queue/mail"</span>, <span class="hljs-keyword">new</span> MessageCreator() {<br>            <span class="hljs-function"><span class="hljs-keyword">public</span> Message <span class="hljs-title">createMessage</span><span class="hljs-params">(Session session)</span> <span class="hljs-keyword">throws</span> JMSException </span>{<br>                <span class="hljs-keyword">return</span> session.createTextMessage(text);<br>            }<br>        });<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>JMS的消息类型支持以下几种：</p>
<ul>
<li>TextMessage：文本消息</li>
<li>BytesMessage：二进制消息</li>
<li>MapMessage：包含多个Key-Value对的消息</li>
<li>ObjectMessage：直接序列化Java对象的消息</li>
<li>StreamMessage：一个包含基本类型序列的消息</li>
</ul>
<p>最常用的是发送基于JSON的文本消息，上述代码通过<code>JmsTemplate</code>创建一个<code>TextMessage</code>并发送到名称为<code>jms/queue/mail</code>的Queue。</p>
<blockquote>
<p>注意：Artemis消息服务器默认配置下会自动创建Queue，因此不必手动创建一个名为<code>jms/queue/mail</code>的Queue，但不是所有的消息服务器都会自动创建Queue，生产环境的消息服务器通常会关闭自动创建功能，需要手动创建Queue。</p>
</blockquote>
<p>注意到<code>MailMessage</code>是我们自定义的一个JavaBean，真正的JMS消息是创建的<code>TextMessage</code>，它的内容是JSON。</p>
<p>当用户注册成功后，我们就调用<code>MessagingService.sendMailMessage()</code>发送一条JMS消息，代码十分简单。</p>
<p>下面我们详细讨论如何处理消息，即编写Consumer。从理论上讲，可以创建另一个Java进程来处理消息，但对于我们这个简单的Web程序来说没有必要，直接在同一个Web应用中接收并处理消息即可。</p>
<p>处理消息的核心代码是编写一个Bean，并在处理方法上标注<code>@JmsListener</code>：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Component</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MailMessageListener</span> </span>{<br>    <span class="hljs-keyword">final</span> Logger logger = LoggerFactory.getLogger(getClass());<br><br>    <span class="hljs-meta">@Autowired</span> ObjectMapper objectMapper;<br>    <span class="hljs-meta">@Autowired</span> MailService mailService;<br><br>    <span class="hljs-meta">@JmsListener(destination = "jms/queue/mail", concurrency = "10")</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onMailMessageReceived</span><span class="hljs-params">(Message message)</span> <span class="hljs-keyword">throws</span> Exception </span>{<br>        logger.info(<span class="hljs-string">"received message: "</span> + message);<br>        <span class="hljs-keyword">if</span> (message <span class="hljs-keyword">instanceof</span> TextMessage) {<br>            String text = ((TextMessage) message).getText();<br>            MailMessage mm = objectMapper.readValue(text, MailMessage.class);<br>            mailService.sendRegistrationMail(mm);<br>        } <span class="hljs-keyword">else</span> {<br>            logger.error(<span class="hljs-string">"unable to process non-text message!"</span>);<br>        }<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>注意到<code>@JmsListener</code>指定了Queue的名称，因此，凡是发送到此Queue的消息都会被这个<code>onMessageReceived()</code>方法处理，方法参数是JMS的Message接口，我们通过强制转型为<code>TextMessage</code>并提取JSON，反序列化后获得自定义的JavaBean，也就获得了发送邮件所需要的信息。</p>
<p>Spring处理JMS消息的流程是什么？</p>
<p>如果我们直接调用JMS的API来处理消息，那么编写的代码大致如下：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-comment">// 创建JMS连接:</span><br>Connection connection = connectionFactory.createConnection();<br><span class="hljs-comment">// 创建会话:</span><br>Session session = connection.createSession(<span class="hljs-keyword">false</span>, Session.AUTO_ACKNOWLEDGE);<br><span class="hljs-comment">// 创建一个Consumer:</span><br>MessageConsumer consumer = session.createConsumer(queue);<br><span class="hljs-comment">// 为Consumer指定一个消息处理器:</span><br>consumer.setMessageListener(<span class="hljs-keyword">new</span> MessageListener() { <br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onMessage</span><span class="hljs-params">(Message message)</span> </span>{<br>        <span class="hljs-comment">// 在此处理消息... </span><br>    }<br>});<br><span class="hljs-comment">// 启动接收消息的循环:</span><br>connection.start();<br></code></pre></td></tr></tbody></table></figure>

<p>我们自己编写的<code>MailMessageListener.onMailMessageReceived()</code>相当于消息处理器：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs java">consumer.setMessageListener(<span class="hljs-keyword">new</span> MessageListener() { <br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onMessage</span><span class="hljs-params">(Message message)</span> </span>{<br>        mailMessageListener.onMailMessageReceived(message); <br>    }<br>});<br></code></pre></td></tr></tbody></table></figure>

<p>所以，Spring根据<code>AppConfig</code>的注解<code>@EnableJms</code>自动扫描带有<code>@JmsListener</code>的Bean方法，并为其创建一个<code>MessageListener</code>把它包装起来。</p>
<p>注意到前面我们还创建了一个<code>JmsListenerContainerFactory</code>的Bean，它的作用就是为每个<code>MessageListener</code>创建<code>MessageConsumer</code>并启动消息接收循环。</p>
<p>再注意到<code>@JmsListener</code>还有一个<code>concurrency</code>参数，10表示可以最多同时并发处理10个消息，<code>5-10</code>表示并发处理的线程可以在5~10之间调整。</p>
<p>因此，Spring在通过<code>MessageListener</code>接收到消息后，并不是直接调用<code>mailMessageListener.onMailMessageReceived()</code>，而是用线程池调用，因此，要时刻牢记，<code>onMailMessageReceived()</code>方法可能被多线程并发执行，一定要保证线程安全。</p>
<p>我们总结一下Spring接收消息的步骤：</p>
<p>通过<code>JmsListenerContainerFactory</code>配合<code>@EnableJms</code>扫描所有<code>@JmsListener</code>方法，自动创建<code>MessageConsumer</code>、<code>MessageListener</code>以及线程池，启动消息循环接收处理消息，最终由我们自己编写的<code>@JmsListener</code>方法处理消息，可能会由多线程同时并发处理。</p>
<p>要验证消息发送和处理，我们注册一个新用户，可以看到如下日志输出：</p>
<figure class="highlight plaintext hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs plaintext">2020-06-02 08:04:27 INFO  c.i.learnjava.web.UserController - user registered: bob@example.com<br>2020-06-02 08:04:27 INFO  c.i.l.service.MailMessageListener - received message: ActiveMQMessage[ID:9fc5...]:PERSISTENT/ClientMessageImpl[messageID=983, durable=true, address=jms/queue/mail, ...]]<br>2020-06-02 08:04:27 INFO  c.i.learnjava.service.MailService - [send mail] sending registration mail to bob@example.com...<br>2020-06-02 08:04:30 INFO  c.i.learnjava.service.MailService - [send mail] registration mail was sent to bob@example.com.<br></code></pre></td></tr></tbody></table></figure>

<p>可见，消息被成功发送到Artemis，然后在很短的时间内被接收处理了。</p>
<p>使用消息服务对发送Email进行改造的好处是，发送Email的能力通常是有限的，通过JMS消息服务，如果短时间内需要给大量用户发送Email，可以先把消息堆积在JMS服务器上慢慢发送，对于批量发送邮件、短信等尤其有用。</p>
<h3 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h3><p>JMS是Java的消息服务，可以通过JMS服务器实现消息的异步处理。消息服务主要解决Producer和Consumer生产和处理速度不匹配的问题。</p>
<h2 id="使用Scheduler"><a href="#使用Scheduler" class="headerlink" title="使用Scheduler"></a>使用Scheduler</h2><p>在许多应用程序中，经常需要执行定时任务。例如，每天或每月给用户发送账户汇总报表，定期检查并发送系统状态报告等等。Java本身就提供了定时执行任务的功能，在Spring中，使用定时任务更简单，不需要手写线程池相关代码，只需要两个注解。</p>
<p>我们还是以实际代码为例，建立工程<code>spring-integration-schedule</code>，无需额外的依赖，我们可以直接在<code>AppConfig</code>中加上<code>@EnableScheduling</code>就开启了定时任务的支持：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Configuration</span><br><span class="hljs-meta">@ComponentScan</span><br><span class="hljs-meta">@EnableWebMvc</span><br><span class="hljs-meta">@EnableScheduling</span> <span class="hljs-comment">// 开启定时任务</span><br><span class="hljs-meta">@EnableTransactionManagement</span><br><span class="hljs-meta">@PropertySource({ "classpath:/jdbc.properties", "classpath:/task.properties" })</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppConfig</span> </span>{<br>    ...<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>然后，我们可以直接在一个Bean中编写一个<code>public void</code>无参数方法，然后加上<code>@Scheduled</code>注解：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Component</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TaskService</span> </span>{<br>    <span class="hljs-keyword">final</span> Logger logger = LoggerFactory.getLogger(getClass());<br><br>    <span class="hljs-meta">@Scheduled(initialDelay = 60_000, fixedRate = 60_000)</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">checkSystemStatusEveryMinute</span><span class="hljs-params">()</span> </span>{<br>        logger.info(<span class="hljs-string">"Start check system status..."</span>);<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>上述注解指定了启动延时60s，并以60s为间隔执行任务。直接运行应用程序，就可以在控制台看到定时任务打印的日志。如果没有看到定时任务的日志，需要检查：</p>
<ul>
<li>是否忘记了在<code>AppConfig</code>中标注<code>@EnableScheduling</code></li>
<li>是否忘记了在定时任务的方法所在的class标注<code>@Component</code></li>
</ul>
<p>除了可以使用<code>fixedRate</code>外，还可以使用<code>fixedDelay</code>。FixedRate是指任务总是以固定的时间间隔触发，不管任务执行多长时间，而FixedDelay是指上一次任务执行完毕后，等待固定的时间间隔，再执行下一次任务。</p>
<p>有的童鞋在实际开发中会遇到一个问题，因为Java的注解全部是常量，写死了<code>fixedDelay=30000</code>，如果根据实际情况要改成60秒怎么办，只能重新编译？</p>
<p>我们可以把定时任务的配置放到配置文件中，例如<code>task.properties</code>：</p>
<figure class="highlight properties hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs properties"><span class="hljs-meta">task.checkDiskSpace</span>=<span class="hljs-string">30000</span><br></code></pre></td></tr></tbody></table></figure>

<p>这样就可以随时修改配置文件而无需动代码。但是在代码中，我们需要用<code>fixedDelayString</code>取代<code>fixedDelay</code>：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Component</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TaskService</span> </span>{<br>    ...<br><br>    <span class="hljs-meta">@Scheduled(initialDelay = 30_000, fixedDelayString = "${task.checkDiskSpace:30000}")</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">checkDiskSpaceEveryMinute</span><span class="hljs-params">()</span> </span>{<br>        logger.info(<span class="hljs-string">"Start check disk space..."</span>);<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>注意到上述代码的注解参数<code>fixedDelayString</code>是一个属性占位符，并配有默认值30000，Spring在处理<code>@Scheduled</code>注解时，如果遇到<code>String</code>，会根据占位符自动用配置项替换，这样就可以灵活地修改定时任务的配置。</p>
<p>此外，<code>fixedDelayString</code>还可以使用更易读的<code>Duration</code>，例如：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Scheduled(initialDelay = 30_000, fixedDelayString = "${task.checkDiskSpace:PT2M30S}")</span><br></code></pre></td></tr></tbody></table></figure>

<p>以字符串<code>PT2M30S</code>表示的<code>Duration</code>就是2分30秒，请参考LocalDateTime一节的Duration相关部分。多个<code>@Scheduled</code>方法完全可以放到一个Bean中，这样便于统一管理各类定时任务。</p>
<h3 id="使用Cron任务"><a href="#使用Cron任务" class="headerlink" title="使用Cron任务"></a>使用Cron任务</h3><p>还有一类定时任务，它不是简单地重复，而是按时间触发，我们把这类任务称为定时任务，比如：</p>
<ul>
<li>每天凌晨2:15执行报表任务</li>
<li>每个工作日12:00执行特定任务</li>
</ul>
<p>Cron源自Unix/Linux系统自带的crond守护进程，以一个简洁的表达式定义任务触发时间。在Spring中，也可以使用Cron表达式来执行Cron任务。Cron表达式详解<a target="_blank" rel="noopener" href="https://www.cnblogs.com/javahr/p/8318728.html">指路</a>。</p>
<p>在Spring中，我们定义一个每天凌晨2:15执行的任务：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Component</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TaskService</span> </span>{<br>    ...<br><br>    <span class="hljs-meta">@Scheduled(cron = "${task.report:0 15 2 * * *}")</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">cronDailyReport</span><span class="hljs-params">()</span> </span>{<br>        logger.info(<span class="hljs-string">"Start daily report task..."</span>);<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>Cron任务同样可以使用属性占位符，这样修改起来更加方便。实际上它可以取代fixedRate类型的定时任务。</p>
<h3 id="集成Quartz"><a href="#集成Quartz" class="headerlink" title="集成Quartz"></a>集成Quartz</h3><p>在Spring中使用定时任务和Cron任务都非常简单，但是要注意到，这些任务的调度都是在每个JVM进程中的。如果本机启动两个进程，或者多台机器上启动应用，这些进程的定时任务和Cron任务都是独立运行互不影响的。</p>
<p>如果一些定时任务要以集群的方式运行，例如每天23:00检查任务，只需要集群中一台运行即可，这时可以考虑使用Quartz。Quartz可以配置一个JDBC数据源，以便存储所有的任务调度计划以及任务执行状态。也可以使用内存来调度任务，但这样配置就和使用Spring的调度没啥区别了，额外集成Quartz的意义就不大。</p>
<p>Quartz的JDBC配置比较复杂，Spring对其也有一定的支持。要详细了解Quartz的集成，请参考Spring的文档。思考：如果不使用Quartz的JDBC配置，多个Spring应用同时运行时，如何保证某个任务只在某一台机器执行？</p>
<h2 id="集成JMX"><a href="#集成JMX" class="headerlink" title="集成JMX"></a>集成JMX</h2><p>什么是JMX？JMX是Java Management Extensions，它是一个Java平台的管理和监控接口。为什么要搞JMX？因为在所有应用程序中，对运行中的程序进行监控是非常重要的，Java应用程序也不例外。我们肯定希望知道Java应用程序当前的状态，例如，占用了多少内存，分配了多少内存，当前有多少活动线程，有多少休眠线程等。</p>
<p>为了标准化管理和监控，Java平台使用JMX作为管理和监控的标准接口，任何程序，只要按JMX规范访问这个接口，就可以获取所有管理和监控信息。实际上，常用的运维监控如Zabbix、Nagios等工具对JVM本身的监控都是通过JMX获取信息。</p>
<p>因为JMX是一个标准接口，不但可以用于管理JVM，还可以管理应用程序自身，下图是JMX的架构：</p>
<figure class="highlight plaintext hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs ascii">    ┌─────────┐  ┌─────────┐<br>    │jconsole │  │   Web   │<br>    └─────────┘  └─────────┘<br>         │            │<br>┌ ─ ─ ─ ─│─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─<br> JVM     ▼            ▼        │<br>│   ┌─────────┐  ┌─────────┐<br>  ┌─┤Connector├──┤ Adaptor ├─┐ │<br>│ │ └─────────┘  └─────────┘ │<br>  │       MBeanServer        │ │<br>│ │ ┌──────┐┌──────┐┌──────┐ │<br>  └─┤MBean1├┤MBean2├┤MBean3├─┘ │<br>│   └──────┘└──────┘└──────┘<br> ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘<br></code></pre></td></tr></tbody></table></figure>

<p>JMX把所有被管理的资源称为MBean（Managed Bean），这些MBean全部由MBeanServer管理，如果要访问MBean，可以通过MBeanServer对外提供的访问接口，例如通过RMI或HTTP访问。注意到使用JMX不需要安装任何额外组件，也不需要第三方库，因为MBeanServer已经内置在JavaSE标准库中了。JavaSE还提供了一个<code>jconsole</code>程序，用于通过RMI连接到MBeanServer，这样就可以管理整个Java进程。</p>
<p>除了JVM会把自身的各种资源以MBean注册到JMX，我们自己的配置、监控信息也可以作为MBean注册到JMX，这样，管理程序就可以直接控制我们暴露的MBean。因此，应用程序使用JMX，只需要两步：</p>
<ol>
<li>编写MBean提供管理接口和监控数据</li>
<li>注册MBean</li>
</ol>
<p>在Spring应用程序中，使用JMX只需要一步：</p>
<ol>
<li>编写MBean提供管理接口和监控数据</li>
</ol>
<p>第二步注册的过程由Spring自动完成。我们以实际工程为例，首先在<code>AppConfig</code>中加上<code>@EnableMBeanExport</code>注解，告诉Spring自动注册MBean：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Configuration</span><br><span class="hljs-meta">@ComponentScan</span><br><span class="hljs-meta">@EnableWebMvc</span><br><span class="hljs-meta">@EnableMBeanExport</span> <span class="hljs-comment">// 自动注册MBean</span><br><span class="hljs-meta">@EnableTransactionManagement</span><br><span class="hljs-meta">@PropertySource({ "classpath:/jdbc.properties" })</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppConfig</span> </span>{<br>    ...<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>剩下的全部工作就是编写MBean。例如，假设我们希望给应用程序添加一个IP黑名单功能，在黑名单中的IP禁止访问，传统的做法是定义一个配置文件，启动的时候读取。如果要修改黑名单怎么办？修改配置文件，然后重启应用程序。但是每次都重启应用程序实在太麻烦了，能不能不重启应用程序？答案是可以的，可以写一个定时读取配置文件的功能，检测到文件改动时自动重新读取。</p>
<p>上述需求的本质是在应用程序运行期间对参数、配置等进行热更新并要求尽快生效。如果以JMX的方式实现，我们不必自己编写自动重新读取等任何代码，只需要提供一个符合JMX标准的MBean来存储配置即可。还是以IP黑名单功能为例，JMX的MBean通常以MBean结尾，我们遵循标准命名规范，首先编写一个<code>BlacklistMBean</code>：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BlacklistMBean</span> </span>{<br>    <span class="hljs-keyword">private</span> Set&lt;String&gt; ips = <span class="hljs-keyword">new</span> HashSet&lt;&gt;();<br><br>    <span class="hljs-keyword">public</span> String[] getBlacklist() {<br>        <span class="hljs-keyword">return</span> ips.toArray(String[]::<span class="hljs-keyword">new</span>);<br>    }<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">addBlacklist</span><span class="hljs-params">(String ip)</span> </span>{<br>        ips.add(ip);<br>    }<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">removeBlacklist</span><span class="hljs-params">(String ip)</span> </span>{<br>        ips.remove(ip);<br>    }<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">shouldBlock</span><span class="hljs-params">(String ip)</span> </span>{<br>        <span class="hljs-keyword">return</span> ips.contains(ip);<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>这个MBean没什么特殊的，和普通Java类没有任何区别。</p>
<p>接下来，我们要使用JMX的客户端来实时热更新这个MBean，要给它加上一些注解，让Spring能根据注解自动把相关方法注册到MBeanServer中：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Component</span><br><span class="hljs-meta">@ManagedResource(objectName = "sample:name=blacklist", description = "Blacklist of IP addresses")</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BlacklistMBean</span> </span>{<br>    <span class="hljs-keyword">private</span> Set&lt;String&gt; ips = <span class="hljs-keyword">new</span> HashSet&lt;&gt;();<br><br>    <span class="hljs-meta">@ManagedAttribute(description = "Get IP addresses in blacklist")</span><br>    <span class="hljs-keyword">public</span> String[] getBlacklist() {<br>        <span class="hljs-keyword">return</span> ips.toArray(String[]::<span class="hljs-keyword">new</span>);<br>    }<br><br>    <span class="hljs-meta">@ManagedOperation</span><br>    <span class="hljs-meta">@ManagedOperationParameter(name = "ip", description = "Target IP address that will be added to blacklist")</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">addBlacklist</span><span class="hljs-params">(String ip)</span> </span>{<br>        ips.add(ip);<br>    }<br><br>    <span class="hljs-meta">@ManagedOperation</span><br>    <span class="hljs-meta">@ManagedOperationParameter(name = "ip", description = "Target IP address that will be removed from blacklist")</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">removeBlacklist</span><span class="hljs-params">(String ip)</span> </span>{<br>        ips.remove(ip);<br>    }<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">shouldBlock</span><span class="hljs-params">(String ip)</span> </span>{<br>        <span class="hljs-keyword">return</span> ips.contains(ip);<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>观察上述代码，BlacklistMBean首先是一个标准的Spring管理的Bean。其次，添加了<code>@ManagedResource</code>表示这是一个MBean，将要被注册到JMX。<code>objectName</code>指定了这个MBean的名字，通常以<code>company:name=Xxx</code>来分类MBean。</p>
<p>对于属性，使用<code>@ManagedAttribute</code>注解标注。上述MBean只有get属性，没有set属性，说明是一个只读属性。对于操作，使用<code>@ManagedOperation</code>注解标注。上述MBean定义了两个操作：<code>addBlacklist()</code>和<code>removeBlacklist()</code>，其他方法如<code>shouldBlock()</code>不会被暴露给JMX。</p>
<p>使用MBean和普通Bean是完全一样的。例如，我们在<code>BlacklistInterceptor</code>对IP黑名单进行拦截：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Order(1)</span><br><span class="hljs-meta">@Component</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BlacklistInterceptor</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">HandlerInterceptor</span> </span>{<br>    <span class="hljs-keyword">final</span> Logger logger = LoggerFactory.getLogger(getClass());<br><br>    <span class="hljs-meta">@Autowired</span><br>    BlacklistMBean blacklistMBean;<br><br>    <span class="hljs-meta">@Override</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">preHandle</span><span class="hljs-params">(HttpServletRequest request, HttpServletResponse response, Object handler)</span></span><br><span class="hljs-function">            <span class="hljs-keyword">throws</span> Exception </span>{<br>        String ip = request.getRemoteAddr();<br>        logger.info(<span class="hljs-string">"check ip address {}..."</span>, ip);<br>        <span class="hljs-comment">// 是否在黑名单中:</span><br>        <span class="hljs-keyword">if</span> (blacklistMBean.shouldBlock(ip)) {<br>            logger.warn(<span class="hljs-string">"will block ip {} for it is in blacklist."</span>, ip);<br>            <span class="hljs-comment">// 发送403错误响应:</span><br>            response.sendError(<span class="hljs-number">403</span>);<br>            <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;<br>        }<br>        <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>最后正常启动Web应用程序，不要关闭它，我们打开另一个命令行窗口，输入<code>jconsole</code>启动JavaSE自带的一个JMX客户端程序。</p>
<p>通过jconsole连接到一个Java进程最简单的方法是直接在Local Process中找到正在运行的<code>AppConfig</code>，点击Connect即可连接当前正在运行的Web应用，在jconsole中可直接看到内存、CPU等资源的监控。点击<code>MBean</code>，左侧按分类列出了所有MBean，可以在java.lang查看内存信息。</p>
<p>在<code>sample</code>中看到我们自己的MBean，单击可查看属性<code>blacklist</code>。点击<code>Operations</code>-<code>addBlacklist</code>，可以填入<code>127.0.0.1</code>并点击<code>addBlacklist</code>按钮，相当于jconsole通过JMX接口，调用了我们自己的<code>BlacklistMBean</code>的<code>addBlacklist()</code>方法，传入的参数就是填入的<code>127.0.0.1</code>。再次查看属性<code>blacklist</code>，就可以看到结果更新了。</p>
<p>使用jconsole连接直接通过Local Process连接JVM有个限制，就是jconsole和正在运行的JVM必须在同一台机器。如果要远程连接，首先要打开JMX端口。我们在启动AppConfig时需要传入以下JVM启动参数：</p>
<ul>
<li>-Dcom.sun.management.jmxremote.port=19999</li>
<li>-Dcom.sun.management.jmxremote.authenticate=false</li>
<li>-Dcom.sun.management.jmxremote.ssl=false</li>
</ul>
<p>第一个参数表示在19999端口监听JMX连接，第二个和第三个参数表示无需验证，不使用SSL连接，在开发测试阶段比较方便，生产环境必须指定验证方式并启用SSL，详细参数可参考Oracle<a target="_blank" rel="noopener" href="https://docs.oracle.com/javase/8/docs/technotes/guides/management/agent.html#gdeum">官方文档</a>。这样jconsole可以用<code>ip:19999</code>的远程方式连接JMX，连接后的操作是完全一样的。许多JavaEE服务器如JBoss的管理后台都是通过JMX提供管理接口，并由Web方式访问，对用户更加友好。</p>
</body></html>
    
    </div>
    
    
    <div class="columns is-mobile is-multiline article-nav">
        <span class="column is-12-mobile is-half-desktop  article-nav-prev">
            
            <a href="/Study/Java/SpringBoot/zh-cn/SpringBoot%E5%BC%80%E5%8F%91/">SpringBoot开发</a>
            
        </span>
        <span class="column is-12-mobile is-half-desktop  article-nav-next">
            
            <a href="/Study/Java/Spring/zh-cn/%E5%BC%80%E5%8F%91Web%E5%BA%94%E7%94%A8/">开发Web应用</a>
            
        </span>
    </div>
    
</article>


<div class="sharebox">
    
<div class="sharethis-inline-share-buttons"></div>
<script type='text/javascript' src='//platform-api.sharethis.com/js/sharethis.js#property=608c1408daac690012507aa2&amp;product=sop' async='async'></script>

</div>



    </div>
</section>
    <footer class="footer">
    <div class="container">
        <div class="columns content">
            <div class="column is-narrow has-text-centered">
                &copy; 2021 wanzixin&nbsp;
                Powered by <a href="http://hexo.io/" target="_blank">Hexo</a> & <a
                        target="_blank" rel="noopener" href="http://github.com/ppoffice/hexo-theme-minos">Minos</a>
            </div>
            <div class="column is-hidden-mobile"></div>

            
            <div class="column is-narrow">
                <div class="columns is-mobile is-multiline is-centered">
                
                    
                <a class="column is-narrow has-text-black" title="GitHub" target="_blank" rel="noopener" href="https://github.com/ppoffice/hexo-theme-minos">
                    
                    GitHub
                    
                </a>
                
                </div>
            </div>
            
            
<div class="column is-narrow has-text-centered">
    <div class="dropdown is-up is-right is-hoverable" style="margin-top: -0.2em;">
        <div class="dropdown-trigger">
            <button class="button is-small" aria-haspopup="true" aria-controls="dropdown-menu7">
                <span class="icon">
                    <i class="fas fa-globe"></i>
                </span>
                <span>简体中文</span>
                <span class="icon is-small">
            <i class="fas fa-angle-down" aria-hidden="true"></i>
          </span>
            </button>
        </div>
        <div class="dropdown-menu has-text-left" role="menu">
            <div class="dropdown-content">
            
                <a href="/Study/Java/Spring/zh-cn/%E9%9B%86%E6%88%90%E7%AC%AC%E4%B8%89%E6%96%B9%E7%BB%84%E4%BB%B6/" class="dropdown-item">
                    English
                </a>
            
                <a href="/zh-cn/Study/Java/Spring/zh-cn/%E9%9B%86%E6%88%90%E7%AC%AC%E4%B8%89%E6%96%B9%E7%BB%84%E4%BB%B6/" class="dropdown-item">
                    简体中文
                </a>
            
            </div>
        </div>
    </div>
</div>

        </div>
    </div>
</footer>
    <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.2/moment-with-locales.min.js"></script>

<!-- test if the browser is outdated -->
<div id="outdated">
    <h6>Your browser is out-of-date!</h6>
    <p>Update your browser to view this website correctly. <a id="btnUpdateBrowser" target="_blank" rel="noopener" href="http://outdatedbrowser.com/">Update my browser now </a></p>
    <p class="last"><a href="#" id="btnCloseUpdateBrowser" title="Close">&times;</a></p>
</div>
<script src="//cdnjs.cloudflare.com/ajax/libs/outdated-browser/1.1.5/outdatedbrowser.min.js"></script>
<script>
    $(document).ready(function () {
        // plugin function, place inside DOM ready function
        outdatedBrowser({
            bgColor: '#f25648',
            color: '#ffffff',
            lowerThan: 'flex'
        })
    });
</script>

<script>
    window.FontAwesomeConfig = {
        searchPseudoElements: true
    }
    moment.locale("zh-CN");
</script>


    
    
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.4/MathJax.js?config=TeX-MML-AM_CHTML"></script>
<script>
    MathJax.Hub.Config({
        "HTML-CSS": {
            matchFontHeight: false
        },
        SVG: {
            matchFontHeight: false
        },
        CommonHTML: {
            matchFontHeight: false
        },
        tex2jax: {
            inlineMath: [
                ['$','$'],
                ['\\(','\\)']
            ]
        }
    });
</script>

    
    
    
    
<script src="//cdnjs.cloudflare.com/ajax/libs/lightgallery/1.6.8/js/lightgallery-all.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/justifiedGallery/3.6.5/js/jquery.justifiedGallery.min.js"></script>
<script>
    (function ($) {
        $(document).ready(function () {
            if (typeof($.fn.lightGallery) === 'function') {
                $('.article.gallery').lightGallery({ selector: '.gallery-item' });
            }
            if (typeof($.fn.justifiedGallery) === 'function') {
                $('.justified-gallery').justifiedGallery();
            }
        });
    })(jQuery);
</script>

    
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.0/clipboard.min.js"></script>
    <style>
        .hljs {
            position: relative;
        }

        .hljs .clipboard-btn {
            float: right;
            color: #9a9a9a;
            background: none;
            border: none;
            cursor: pointer;
        }

        .hljs .clipboard-btn:hover {
          color: #8a8a8a;
        }

        .hljs > .clipboard-btn {
            display: none;
            position: absolute;
            right: 4px;
            top: 4px;
        }

        .hljs:hover > .clipboard-btn {
            display: inline;
        }

        .hljs > figcaption > .clipboard-btn {
            margin-right: 4px;
        }
    </style>
    <script>
      $(document).ready(function () {
        $('figure.hljs').each(function(i, figure) {
          var codeId = 'code-' + i;
          var code = figure.querySelector('.code');
          var copyButton = $('<button>Copy <i class="far fa-clipboard"></i></button>');
          code.id = codeId;
          copyButton.addClass('clipboard-btn');
          copyButton.attr('data-clipboard-target-id', codeId);

          var figcaption = figure.querySelector('figcaption');

          if (figcaption) {
            figcaption.append(copyButton[0]);
          } else {
            figure.prepend(copyButton[0]);
          }
        })

        var clipboard = new ClipboardJS('.clipboard-btn', {
          target: function(trigger) {
            return document.getElementById(trigger.getAttribute('data-clipboard-target-id'));
          }
        });
        clipboard.on('success', function(e) {
          e.clearSelection();
        })
      })
    </script>

    
    

    



<script src="/js/script.js"></script>


    
    <div class="searchbox ins-search">
    <div class="searchbox-mask"></div>
    <div class="searchbox-container ins-search-container">
        <div class="searchbox-input-wrapper">
            <input type="text" class="searchbox-input ins-search-input" placeholder="站内搜索" />
            <span class="searchbox-close ins-close ins-selectable"><i class="fa fa-times-circle"></i></span>
        </div>
        <div class="searchbox-result-wrapper ins-section-wrapper">
            <div class="ins-section-container"></div>
        </div>
    </div>
</div>
<script>
    (function (window) {
        var INSIGHT_CONFIG = {
            TRANSLATION: {
                POSTS: '文章',
                PAGES: '页面',
                CATEGORIES: '分类',
                TAGS: '标签',
                UNTITLED: '(无标题)',
            },
            CONTENT_URL: '/content.zh-cn.json',
        };
        window.INSIGHT_CONFIG = INSIGHT_CONFIG;
    })(window);
</script>

<script src="/js/insight.js"></script>

    
</body>
</html>